Tutoriel HsIndex

Tutoriel HsIndex 3 - Lecture du fichier d'index

Lecture du fichier d'index

Afin de lire les fichiers d'index, il est nécessaire de créer un parser (analyseur syntaxique). J'utilise pour cela la bibliothèque parsec disponible avec l'installation standard de Haskell-plateform. Cette bibliothèque est bien conçue et contient toutes les fonctions pour générer des parser du plus simple au plus complexe.

Format d'un fichier d'index

Un fichier d'index se présente sous la forme suivante

Pour un index en français:

\indexentry{cadre|hyperpage}{9}
\indexentry{charpente!ossature|hyperpage}{9}
\indexentry{charpente!structure|hyperpage}{9}
\indexentry{carburant|hyperpage}{9}

Ou encore pour un index en russe:

\indexentry{\IeC {\cyrs }\IeC {\cyrr }\IeC {\cyre }\IeC {\cyrd }\IeC {\cyrn }\IeC {\cyre }\IeC {\cyre }|hyperpage}{6}
\indexentry{\IeC {\cyra }\IeC {\cyrk }\IeC {\cyrs }\IeC {\cyri }\IeC {\cyro }\IeC {\cyrm }|hyperpage}{6}
\indexentry{\IeC {\cyro }\IeC {\cyrs }\IeC {\cyrsftsn }|hyperpage}{6}
\indexentry{\IeC {\cyrb }\IeC {\cyra }\IeC {\cyrb }\IeC {\cyrb }\IeC {\cyri }\IeC {\cyrt }|hyperpage}{6}

Lecture de la commande `\indexentry`

Le parser devra relire la commande \indexentry qui se compose comme suit :

  1. Premier argument LaTeX

    • Une entrée d'index.

    • Des sous-entrées d'index séparées par des points d'exclamation !.

    • Une barre verticale suivie de l'étiquette hyperpage.

  2. Deuxième argument LaTeX

    • Le numéro de page.

Commande `\indexentry` avec une entrée simple

Voyons comment analyser une ligne de commande simple (pas de sous-entrées)

Tout d'abord, on utilise la fonction string de Parsec qui impose une chaîne de caractères.

  string "\\indexentry"

Le paquet imakeidx génère parfois des espaces entre le début de commandes et les arguments. Pour sauter ces espaces on utilise la fonction de recherche char ' ' en association avec le combinateur many qui permet de rechercher (facultativement) des occurrences du parser passé en argument.

  many (char ' ')

pour le contenu entre accolades on pourrait faire deux scans avec char '{' et char '}' comme ceci :

  char '{'
  ...
  char '}'

Mais il est préférable d'utiliser la fonction between de Parsec qui permet de parser des éléments entre deux motifs. Et dans la mesure ou ce motif sera réutilisé par la suite, il vaut mieux créer une fonction dédiée pour parser ce motif:

braces = between (char '{') (char '}')

Pour scanner le contenu de l'index, on appliquera plusieurs fois de manière successive une série de parser spécifiques pour chaque caractère pour prendre en compte les caractères spéciaux du paquet imakeidx.

Pour cela on utilisera le combinateur many1 pour rechercher (impérativement) une ou plusieurs occurrences du contenu. Le combinateur choice permet de tester des parser l'un après l'autre et de renvoyer le résultat du premier parser qui réussit. Ici, on utilise la fonction <- qui permet de récupérer le résultat de parsing et de le conserver.

itm <- many1 (choice pars)

On continue ensuite par détecter la chaîne de caractères :

string "|hyperpage"

Et enfin le deuxième argument contenant le numéro de page. Ici aussi, on conserve le résultat du parser avec la fonction <- :

  n <- braces (many1 digit)

La fonction finale sera donc :

parseIDX :: [Parser Char] -> Parser IndexItem
parseIDX pars = do
  string "\\indexentry"
  many (char ' ')
  itm <- braces (do itm <- many1 (choice pars)
                    string "|hyperpage"
                    return itm)
  many (char ' ')
  n <- braces (many1 digit)
  return (IndexItem itm  (Letters, itm) [read n] [])

return permet de retourner le résultat de la fonction directement avec le type IndexItem

Commande `\indexentry` avec des sous-entrées

Pour lire les commandes \indexentry avec des sous-entrées, on utilisera deux versions modifiées de la fonction parseIDX :

parseIDXSub :: [Parser Char] -> Parser IndexItem
parseIDXSub pars = do
  string "\\indexentry"
  many (char ' ')
  (itm, sub) <- braces (do  itm <- many1 (choice pars)
                            char '!'
                            sub <- many1 (choice pars)
                            string "|hyperpage"
                            return (itm, sub))
  many (char ' ')
  n <- braces (many1 digit)
  return (IndexItem itm  (Letters, itm) [] [IndexSubItem sub sub [read n] []])


parseIDXSubSub :: [Parser Char] -> Parser IndexItem
parseIDXSubSub pars = do
  string "\\indexentry"
  many (char ' ')
  (itm, sub, ssub) <- braces (do  itm <- many1 (choice pars)
                                  char '!'
                                  sub <- many1 (choice pars)
                                  char '!'
                                  ssub <- many1 (choice pars)
                                  string "|hyperpage"
                                  return (itm, sub, ssub))
  many (char ' ')
  n <- braces (many1 digit)
  return (IndexItem itm  (Letters, itm) [] [IndexSubItem sub sub [] [IndexSubSubItem ssub ssub [read n]]])

On essaiera d'appliquer successivement ces fonctions en utilisant de façon combinée l'opérateur <|> qui permet d'appliquer plusieurs parser de manière successive jusqu'à ce qu'un parser réussisse et la fonction try qui permet de tester un parser sans consommer de caractères (Voir la notice de Parsec les explications détaillées sur son fonctionnement).

On aura alors :

parseIndexItem pars =  try (parseIDXSubSub pars)
                   <|> try (parseIDXSub pars)
                   <|>     (parseIDX pars)

Lecture des caractères non ascii (version optimisée)

Le parser devra également relire les commandes LaTeX permettant de générer les caractères étrangers (non-ascii). En effet, le paquet imakeidx génère du code LaTeX en utilisant des caractères ASCII (et pas Unicode). Il est donc nécessaire de convertir ces commandes en caractères Unicode correspondant. Par exemple, la commande \IeC {\^e } permet de générer le caractère ê et la commande \IeC {\CYRD } permet de générer la lettre cyrillique Д.

Une liste de correspondance sera créée en dur dans le code sous la forme:

lstSubs :: [(String, Char)]
lstSubs = 
  [
    ("\\^e", "ê")
  , ("\\'e", "é")
  
  ...
  ]

La commande \IeC commune à toutes commandes sera traitée dans une fonction prenant comme argument une liste de correspondance de chaîne de caractères à tester avec le caractère Unicode équivalent.

lstParseIeC lst = try $ do
  string "\\IeC"
  many (char ' ')
  braces $ do
        many (char ' ')
        char '\\'
        choice $ map
          (\(s, r) -> try $ do
              string s
              many (char ' ')
              return r
          )
          lst  

Comme on peut le voir, la fonction :

  1. Recherche la chaîne de caractères \IeC.

  2. Saute de possibles espaces.

  3. Cherche des accolades ouvrantes et fermantes (après avoir lancé le parser en argument) avec la fonction braces.

  4. Le caractère \.

  5. Une succession de tests :

    1. Test de la chaîne de caractères.

    2. Retour du caractère unicode équivalent si le test réussi avec return.

  6. Si le parser réussi, la fonction retournera le caractère passé en argument .

Fonction finale

La fonction que l'on appellera en amont sera :

parseIndexFile :: Maybe [Parser Char] -> Parser [IndexItem]
parseIndexFile Nothing = do
  emptyLines
  itms <- endBy (parseIndexItem parseCharL) endOfLineP
  emptyLines
  eof
  return itms

Avec quelques fonctions annexes :

emptyLines = many emptyLine

emptyLine = do
  many (oneOf " \t")
  endOfLineP

endOfLineP :: Parser String
endOfLineP =    try (string "\n")
            <|> try (string "\r\n")

La fonction parseIndexFile commence par sauter des lignes vides (facultatives) et à capturer des entrées de lexiques séparées par des fin de lignes. La fonction endBy permet de rechercher et de capturer des motifs séparés et terminés par le même motif. La fonction se termine après avoir sauté des lignes vides (facultatives) et détecté la fin du fichier avec eof.

La fonction endOfLineP permet de détecter une fin de ligne au format Unix/Linux (Caractère newline \n simple ) ou une fin de ligne au format Window (Caractère retour chariot \r suivi de nouvelle ligne \n).

La fonction emptyLine recherche avec many des occurrences (facultatives) d'espaces et de tabulations qui se termine par une fin de ligne Unix ou Window avec endOfLineP. Le choix des caractères acceptés se fait avec la fonction oneOf.

La fonction empyLines est simplement une recherche de plusieurs lignes vides.

Voila pour l'analyseur syntaxique du fichier d'index.